﻿using Sharp.Interprocess.MemoryManager;
using System;
using System.Collections.Generic;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace Sharp.Interprocess
{
    public class MemoryMappedFileStore : IDisposable
    {
        #region Constants

        private const string MUTEX_NAME_SUFFIX = "_MemoryMappedFileStorage_Mutex";
        private const string MEMORY_MAPPED_FILE_NAME_SUFFIX = "_MemoryMappedFile_TheObjectItself";
        private const int MUTEX_TIMEOUT_MILLISECONDS = 60000; // 1 min.
        private const int MUTEX_WAITONE_RETRY_COUNT = 15;

        #endregion

        #region Private Members

        private IMemoryManager m_MemoryManager;
        private MemoryMappedFile m_MemoryMappedFile;
        private Mutex m_SyncObject;

        #endregion

        #region Constructor(s)
        public MemoryMappedFileStore(IMemoryManager memoryManager)
        {
            if (memoryManager == null)
            {
                throw new ArgumentNullException("memoryManager");
            }
            if (String.IsNullOrWhiteSpace(memoryManager.MemoryManagerId))
            {
                throw new ArgumentException("Invalid memory manager Id");
            }

            m_MemoryManager = memoryManager;            

            m_SyncObject = new Mutex(false, m_MemoryManager.MemoryManagerId + MUTEX_NAME_SUFFIX);
            m_MemoryMappedFile = MemoryMappedFile.CreateOrOpen(m_MemoryManager.MemoryManagerId + MEMORY_MAPPED_FILE_NAME_SUFFIX, m_MemoryManager.TotalMemorySize);
        }
        #endregion

        #region Public Methods

        public void Save<T>(T objectInstance,SegmentHandle objectSegmentHandle)
        {
            var serializedObject = SerializationUtil.Serialize(objectInstance);
            Save(serializedObject, objectSegmentHandle);
        }

        public SegmentHandle AllocateAndSave<T>(T objectInstance)
        {
            if (!typeof(T).IsPrimitive && Object.Equals(objectInstance,default(T)))
            {
                throw new ArgumentException("objectInstance should not be null");
            }

            var serializedObject = SerializationUtil.Serialize(objectInstance);
            var allocatedSegmentHandle = m_MemoryManager.Allocate(serializedObject.Length);

            Save(serializedObject, allocatedSegmentHandle);
            return allocatedSegmentHandle;
        }

        public void Deallocate(SegmentHandle segmentHandle)
        {
            m_MemoryManager.Free(segmentHandle);
        }

        public bool TryLoad<T>(SegmentHandle segmentHandle,out T loadedObject)
        {
            var serializedObject = Load(segmentHandle);
            var operationSucceeded = false;

            if (!serializedObject.Any())
            {
                loadedObject = default(T);
            }
            else
            {
                try
                {
                    loadedObject = SerializationUtil.Deserialize<T>(serializedObject.ToArray());
                    operationSucceeded = true;
                }
                catch (Exception e)
                {
                    throw new ApplicationException(String.Format("Failed to deserialize object loaded from segment. Invalid data stored in the segment? (handle Id = {0})", segmentHandle.HandleId), e);
                }
            }

            return operationSucceeded;
        }

        #endregion

        #region Helper Methods

        protected IEnumerable<byte> Load(SegmentHandle objectSegmentHandle)
        {
            var serializedObject = new List<byte>();

            try
            {
                m_SyncObject.WaitOneWithRetries(MUTEX_TIMEOUT_MILLISECONDS, MUTEX_WAITONE_RETRY_COUNT);
                foreach (var segment in objectSegmentHandle.Segments.OrderBy(s => s.SegmentPartId))
                {
                    var segmentData = m_MemoryMappedFile.ReadDataArray<byte>(Convert.ToInt32(segment.SegmentOffset), segment.SegmentSize);
                    serializedObject.AddRange(segmentData);
                }
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }

            return serializedObject;
        }

        protected void Save(byte[] serializedObject, SegmentHandle objectSegmentHandle)
        {
            try
            {
                m_SyncObject.WaitOneWithRetries(MUTEX_TIMEOUT_MILLISECONDS,MUTEX_WAITONE_RETRY_COUNT);
                var serializedObjectSubsetOffset = 0;
                foreach (var segment in objectSegmentHandle.Segments.OrderBy(s => s.SegmentPartId))
                {
                    var serializedObjectSubset = serializedObject.Skip(serializedObjectSubsetOffset)
                                                                 .Take(segment.SegmentSize)
                                                                 .ToArray();

                    m_MemoryMappedFile.WriteDataArray(Convert.ToInt32(segment.SegmentOffset), serializedObjectSubset);
                    serializedObjectSubsetOffset += segment.SegmentSize;
                }
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }
        }


        #endregion

        #region IDisposable Implementation

        protected virtual void DisposeThisInstance()
        {
            if (m_MemoryMappedFile != null)
            {
                m_MemoryMappedFile.Dispose();
            }
            
            var disposableMemoryManager = m_MemoryManager as IDisposable;
            if (disposableMemoryManager != null)
            {
                disposableMemoryManager.Dispose();
            }

            if (m_SyncObject != null)
            {
                m_SyncObject.Dispose();
            }
        }

        public void Dispose()
        {
            DisposeThisInstance();
            GC.SuppressFinalize(this);
        }

        ~MemoryMappedFileStore()
        {            
            DisposeThisInstance();
        }

        #endregion

    }
}
